unit untFunctions;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils; 

function AutoJustifyText(const AText: String): String;

implementation

function AutoJustifyText(const AText: String): String;
var InBuffer: String;
    Output: TStringList;
    InComment: Boolean;
    IndentString, Line: String;
    i, j: Integer;
    // Delete the string starting with ACommand and followed by a string of
    // {...} and [...] from AText and return the deleted part
    // Example: ExtractCommand('\test', '\test{1}{2}[3]{4}xyz') gives '\text{1}{2}[3]{4}'
    //  and sets AText to 'xyz'
    function ExtractCommand(const ACommand: String; var AText: String): String;
    var i: Integer;
    begin
      Result := ACommand;
      AText := Copy(AText, Length(ACommand) + 1, Length(AText));
      while (AText <> '') and (AText[1] in ['{', '[']) do
      begin
        // This is dangerous in constructs like \begin{$a = e^{x}$}, but who's going to use that?
        if AText[1] = '{' then i := Pos('}', AText);
        if AText[1] = '}' then i := Pos(']', AText);
        if i = -1 then Exit;
        Result := Result + Copy(AText, 1, i);
        AText := Copy(AText, i + 1, Length(AText));
      end;
    end;
    procedure DoOutput;
    begin
      Line := Trim(Line);
      if Line = '' then Exit;
      if InComment and (Copy(Line, 1, 1) <> '%') then Line := '% ' + Trim(Line);
      Output.Add(IndentString + Trim(Line));
      if Pos('%', Line) > 0 then InComment := True;
      Line := '';
    end;
begin
  Result := AText;
  InBuffer := TrimRight(AText);
  if InBuffer = '' then Exit;
  InComment := False;
  Output := TStringList.Create;
  Output.Add('');
  try
    // If there are CR/LF characters, keep them
    while (Length(InBuffer) > 0) and (InBuffer[1] in [#10, #13]) do
      Delete(InBuffer, 1, 1);
    // Take the number of spaces at the beginning to be the indent level
    IndentString := '';
    while (Length(InBuffer) > 0) and (InBuffer[1] = ' ') do begin
      IndentString := IndentString + ' ';
      Delete(InBuffer, 1, 1);
    end;
    Line := '';
    while InBuffer <> '' do begin
      // Put \begin{...} and \end{...} on a separate line,
      // increasing and decreasing the indent level as necessary
      if Copy(InBuffer, 1, 6) = '\begin' then
      begin
        DoOutput();
        Output.Add(IndentString + ExtractCommand('\begin', InBuffer));
        IndentString := IndentString + '  ';
      end;
      if Copy(InBuffer, 1, 4) = '\end' then
      begin
        DoOutput();
        Delete(IndentString, 1, 2);
        Output.Add(IndentString + ExtractCommand('\end', InBuffer));
      end;
      // Start a new line on \[, and after \]
      if Copy(InBuffer, 1, 2) = '\[' then
        DoOutput();
      if Copy(InBuffer, 1, 2) = '\]' then
      begin
        Line := Line + '\]';
        Delete(InBuffer, 1, 2);
        DoOutput;
        Continue;
      end;
      // Find the next word: find the next space
      // If not found, next CR or LF. If that is not found either, use entire string
      i := Pos(' ', InBuffer);  // find space
      j := Pos(#13, InBuffer);  // find #13
      if (i = 0) or ((j > 0) and (j < i)) then i := j; // no space, or #13 before ' '
      j := Pos(#10, InBuffer); // find #10
      if (i = 0) or ((j > 0) and (j < i)) then i := j; // no space and #13, or #10 before ' ' or #13
      if i = 0 then i := Length(InBuffer); // no space, #13 or #10

      // If the line would become too long, break it now (unless Line = '')
      if (Line <> '') and (Length(Line) + Length(IndentString) + i >= 80) then
        DoOutput();
      // Add the word to the line
      Line := Trim(Line) + ' ' + Trim(Copy(InBuffer, 1, i));
      // If the separator was CR or LF and we are in a comment, output the entire line
      if InComment and (InBuffer[i] in [#10, #13]) then
      begin
        DoOutput;
        InComment := False;
      end;
      // Remove the part we added or output from InBuffer
      Delete(InBuffer, 1, i);
    end;
    // If after all of this there is buffered output left, add to the result
    DoOutput();
    // Make it a complete string again
    Result := Output.Text;
  finally
    Output.Free;
  end;
end;

end.

